package fr.inserm.umr915.libjson;


import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.w3c.dom.Document;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;


/**
 * abstract JSON Node
 */
public abstract class Node
	implements Cloneable,Comparable<Node>,JSONable
	{
	protected Node()
		{
		}
	@Override
	public final boolean equals(Object obj)
		{
		if(obj==this) return true;
		if(obj==null || !(obj instanceof Node)) return false;
		return compareTo(Node.class.cast(obj))==0;
		}
	public abstract NodeType getNodeType();
	public abstract NodeFactory getNodeFactory();
	public abstract Node clone(NodeFactory owner);
	
	@Override
	public Node asJson(NodeFactory factory)
		{
		return clone(factory);
		}
	
	public boolean isA(NodeType t)
		{
		return getNodeType()==t;
		}
	
	public boolean isBoolean() { return isA(NodeType.BOOLEAN);}
	public boolean isNil() { return isA(NodeType.NIL);}
	public boolean isInteger() { return isA(NodeType.INT);}
	public boolean isFloat() { return isA(NodeType.FLOAT);}
	public boolean isString() { return isA(NodeType.STRING);}
	public boolean isArray() { return isA(NodeType.ARRAY);}
	public boolean isObject() { return isA(NodeType.OBJECT);}
	
	public ObjectNode asObjectNode() { return ObjectNode.class.cast(this);}
	public ArrayNode asArrayNode() { return ArrayNode.class.cast(this);}
	public IntNode asIntNode() { return IntNode.class.cast(this);}
	public FloatNode asFloatNode() { return FloatNode.class.cast(this);}
	public StringNode asStringNode() { return StringNode.class.cast(this);}
	public BooleanNode asBooleanNode() { return BooleanNode.class.cast(this);}
	
	
	public ArrayNode asArrayNode(NodeType...types)
		{
		ArrayNode a=asArrayNode();
		ArrayNode b=getNodeFactory().createArrayNode(a.size());
		for(Node n:a)
			{
			for(NodeType t: types)
				{
				if(n.getNodeType()==t)
					{
					b.add(n);
					break;
					}
				}
			}
		return b;
		}
	
	@Override
	public Object clone()
		{
		return clone(getNodeFactory());
		}
	public abstract void write(DataOutputStream out) throws IOException;
	public abstract void write(XMLStreamWriter out) throws XMLStreamException;
	
	public byte[] toByteArray()
		{
		try
			{
			ByteArrayOutputStream baos=new ByteArrayOutputStream();
			DataOutputStream daos=new DataOutputStream(baos);
			write(daos);
			daos.flush();
			baos.flush();
			baos.close();
			return baos.toByteArray();
			}
		catch (Exception err)
			{
			throw new RuntimeException(err);
			}
		}
	
	public String getJson()
		{
		try {
			StringWriter str=new StringWriter();
			PrintWriter p=new PrintWriter(str);
			print(p);
			p.flush();
			return str.toString();
		} catch (Exception e) {
			throw new RuntimeException(e);
			}
		}
	
	public abstract void print(PrintWriter out);
	@Override
	public int compareTo(Node o)
		{
		if(o==null) return -1;
		if(o==this) return 0;
		if(getNodeType()!=o.getNodeType())
			{
			return getNodeType().compareTo(o.getNodeType());
			}
		switch(getNodeType())
			{
			case NIL: return 0;
			case BOOLEAN:
				return (asBooleanNode().getValue()?1:0)-(o.asBooleanNode().getValue()?1:0);
			case FLOAT:
				return new Double(asFloatNode().getValue()).compareTo(o.asFloatNode().getValue());
			case INT:
				return asIntNode().getValue()- o.asIntNode().getValue();
			case STRING:
				return getNodeFactory().getStringComparator().compare(
						asStringNode().getValue(),
						o.asStringNode().getValue()
						);
			case ARRAY:
				{
				int n1= asArrayNode().size();
				int n2= o.asArrayNode().size();
				if(n1!=n2) return n1-n2;
				for(int i=0;i< n1;++i)
					{
					int d= asArrayNode().get(i).compareTo(o.asArrayNode().get(i));
					if(d!=0) return d;
					}
				return 0;
				}
			case OBJECT:
				{
				ObjectNode o1=asObjectNode();
				ObjectNode o2=o.asObjectNode();
				int n1= o1.size();
				int n2= o2.size();
				if(n1!=n2) return n1-n2;
				Set<String> set1=new TreeSet<String>(o1.getMap().keySet());
				Set<String> set2=new TreeSet<String>(o2.getMap().keySet());
				n1=set1.size();
				n2=set2.size();
				if(n1!=n2) return n1-n2;
				Iterator<String> r1=set1.iterator();
				Iterator<String> r2=set1.iterator();
				while(r1.hasNext())
					{
					String s1=r1.next();
					String s2=r2.next();
					int i=s1.compareTo(s2);
					if(i!=0) return i;
					}
				for(String s1:o1.getMap().keySet())
					{
					Node x1= o1.get(s1);
					Node x2= o2.get(s1);
					int i=x1.compareTo(x2);
					if(i!=0) return i;
					}
				return 0;
				}
			default: throw new IllegalStateException();
			}
		}
	public org.w3c.dom.Document asDom()
		{
		try {
			DocumentBuilderFactory f=DocumentBuilderFactory.newInstance();
			f.setIgnoringComments(true);
			f.setNamespaceAware(false);
			f.setValidating(false);
			Document dom=f.newDocumentBuilder().newDocument();
			dom.appendChild(asDom(dom));
			return dom;
			} 
		catch (Exception e)
			{
			throw new RuntimeException(e);
			}
		}
	public abstract org.w3c.dom.Element asDom(Document owner);
	
	public final void parse(ContentHandler handler) throws SAXException,IOException
		{
		handler.startDocument();
		saxHandler(handler);
		handler.endDocument();
		}
	protected abstract void saxHandler(ContentHandler handler) throws SAXException,IOException;
	
	
	}

